# !/usr/bin/python3
# -*- coding: utf-8 -*-
# @Time    : 2020/4/22 9:54
# @Author  : zjw
# @FileName: logRegres.py.py
# @Software: PyCharm
# @Blog    ：https://www.cnblogs.com/vanishzeng/


from math import *
from numpy import *
import matplotlib.pyplot as plt


def loadDataSet():
    """
    读取测试文件中的数据，拆分得到的每一行数据并存入相应的矩阵中，最后返回。
    :return: dataMat, labelMat
        dataMat: 特征矩阵
        labelMat: 类型矩阵
    """

    # 初始化特征列表和类型列表
    dataMat = []
    labelMat = []
    # 打开测试数据，默认为读方式
    fr = open("data/testSet.txt")
    # 通过readLines()方法可以获得文件中的所有行信息
    for line in fr.readlines():
        # 将一行中大的信息先通过strip()方法去掉首尾空格
        # 再通过split()方法进行分割，默认分割方式是空格分割
        # 将分割好后的数据存入列表lineArr
        lineArr = line.strip().split()
        # 取出列表lineArr中的数据通过append方法插入到dataMat列表表中
        # 这里为了方便计算，将第一列的值都设置为1.0
        dataMat.append([1.0, float(lineArr[0]), float(lineArr[1])])
        # 再取出文件中的最后一列的值作为类型值存入labelMat列表中
        labelMat.append(int(lineArr[2]))
        """
        列表中的append方法小结：
            从上面两个列表添加元素的语句可以看出：
            当我们需要添加一行数据时，需要加[]，表示将插入一行数据。
            当我们将数据直接插入到某一列的后面时，则不需要加[]。
            且需要注意的是：如果一次要添加好几个元素时，必须有[],否则会报错。
        """
    return dataMat, labelMat


def sigmoid(inX):
    """
    sigmoid函数：α(z) = 1 / (1 + e^(-z))
    :param inX: 函数中的参数z
    :return: 返回函数计算结果
    """
    return 1.0 / (1 + exp(-inX))


def gradAscent(dataMatIn, classLabels):
    """
    梯度上升算法
    :param dataMatIn: 特征值数组
    :param classLabels: 类型值数组
    :return: 返回最佳回归系数
    """

    # 通过numpy模块中的mat方法可以将列表转化为矩阵
    dataMatrix = mat(dataMatIn)
    # transpose()方法是矩阵中的转置
    labelMatrix = mat(classLabels).transpose()
    # 通过numpy中大的shape方法可以获得矩阵的行数和列数信息
    # 当矩阵是一维矩阵时，返回的是一个数的值
    # 当矩阵是二维矩阵时，返回的是一个（1*2）的元组，元组第一个元素表示行数，第二个元素表示列数
    m, n = shape(dataMatrix)  # m = 100; n = 3
    alpha = 0.001  # 步长
    maxCycles = 500  # 迭代次数
    # ones()属于numpy模块，函数功能是生成一个所有元素都为1的数组
    # 这里是生成一个“n行1列”的数组，数组中每一个元素值都是1
    weights = ones((n, 1))
    # 循环迭代maxCycles次，寻找最佳参数
    for k in range(maxCycles):
        # dataMatrix * weights 是矩阵乘法
        # 矩阵相乘时注意第一个矩阵的列数要和第二个矩阵的行数相同
        # (m × n) * ( n × 1) = (m × 1) 括号中表示几行几列
        # (100 × 3) * (3 × 1) = (100 × 1)
        # 最后得到一个100行1列的矩阵
        # 该矩阵运算结果为为sigmoid函数（α(z) = 1 / (1 + e^(-z))）中的z
        # z的公式为：z = w0x0 + w1x1 + w2x2 + ... + wnxn
        h = sigmoid(dataMatrix * weights)
        # 计算真实类别与预测类别的差值
        error = labelMatrix - h
        # 按差值error的方向调整回归系数
        # 0.01 * (3 × m) * (m × 1)
        # 表示每一个列上的一个误差情况，最后得到x1，x2，xn的系数偏移量
        # 矩阵乘法，最后得到一个更新后的回归系数
        # 梯度上升算法公式：w:=w+α▽w f(w)
        # 其中α是步长，▽w f(w)是上升方向
        weights = weights + alpha * dataMatrix.transpose() * error
    return array(weights)


def stocGradAscent0(dataMatrix, classLabels):
    """
    随机梯度上升算法
    :param dataMatrix: 特征值矩阵
    :param classLabels: 类型数组
    :return: 最佳系数weights
    """

    # m = 100，n = 3
    m, n = shape(dataMatrix)
    alpha = 0.01
    weights = ones(n)
    print(weights)
    # 循环迭代m次，即100次
    for i in range(m):
        # (1 × 3) * (1 × 3) = (1 × 3)
        # 数组相乘，两个数组的每个元素对应相乘
        # 最后求和
        # z = w0x0 + w1x1 + wnxn
        # 在将z代入sigmoid函数进行计算
        h = sigmoid(sum(dataMatrix[i] * weights))
        # 计算实际结果和测试结果之间的误差，按照差值调整回归系数
        error = classLabels[i] - h
        # 通过梯度上升算法更新weights
        weights = weights + alpha * error * dataMatrix[i]
    return weights


def stocGradAscent1(dataMatrix, classLabels, numIter=150):
    """
    改进后的随机梯度上升算法
    :param dataMatrix: 特征值矩阵
    :param classLabels: 类型数组
    :param numIter: 迭代次数，设置默认值为150
    :return: 最佳系数weights
    """

    m, n = shape(dataMatrix)
    # 创建与列数相同矩阵，所有元素都为1
    weights = ones(n)
    # 随机梯度，循环默认次数为150次，观察是否收敛
    for j in range(numIter):
        # 产生列表为[0, 1, 2 ... m-1]
        dataIndex = list(range(m))
        for i in range(m):
            # i和j不断增大，导致alpha不断减小，单上衣不为0,
            # alpha会随着迭代不断的减小，但永远不会减小到0，因为后面还有一个常数项0.01
            alpha = 4 / (1.0 + j + i) + 0.01
            # 产生一个随机在0-len()之间的值
            # random.uniform(x, y)方法将随机生成一个实数，他在[x, y]范围内，x<=y。
            randIndex = int(random.uniform(0, len(dataIndex)))
            # sum(dataMatrix[randIndex]*weights)是为了求z值
            # z = w0x0 + w1x1 + ... + wnxn
            h = sigmoid(sum(dataMatrix[randIndex] * weights))
            # 计算实际结果和测试结果之间的误差,按照差值调整回归系数
            error = classLabels[randIndex] - h
            # 通过梯度上升算法更新weights
            weights = weights + alpha * error * dataMatrix[randIndex]
            # 删除掉此次更新中用到的特征数据
            del(dataIndex[randIndex])
    return weights


def plotBestFit(dataArr, labelMat, weights):
    """
    画出决策边界
    :param dataArr: 特征值数组
    :param labelMat: 类型数组
    :param weights: 最佳回归系数
    :return:
    """

    # 通过shape函数获得dataArr的行列数，其中[0]即行数
    n = shape(dataArr)[0]
    # xCord1和yCord1是类型为1的点的x和y坐标值
    xCord1 = []
    yCord1 = []
    # xCord2和yCord1是类型为0的点的x和y坐标值
    xCord2 = []
    yCord2 = []
    # 特征数组的每一行和类型数组的没每一列一一对应
    for i in range(n):
        # 当类型为1时，
        # 将特征数组中的指定行的1和2两个下标下的值分别作为x轴和y轴的值
        if int(labelMat[i]) == 1:
            xCord1.append(dataArr[i, 1])
            yCord1.append(dataArr[i, 2])
        # 当类型为0时，
        # 将特征数组中的指定行的1和2两个下标下的值分别作为x轴和y轴的值
        else:
            xCord2.append(dataArr[i, 1])
            yCord2.append(dataArr[i, 2])
    # figure()操作时创建或者调用画板
    # 使用时遵循就近原则，所有画图操作是在最近一次调用的画图板上实现。
    fig = plt.figure()
    # 将fig分成1×1的网格，在第一个格子中加载ax图
    # 参数111表示“1×1网格中的第1个表格”
    # 如果参数是211则表示“2行1列的表格的中的第一个表格”
    # 第几个表格的计算顺序为从左到右，从上到下
    ax = fig.add_subplot(111)
    # 设置散点图参数
    # 前两个参数xCord1，yCord1表示散点对应的x和y坐标值
    # s=30表示散点大小为30
    # c='red'表示散点颜色为红色
    # marker='s'表示散点的形状，这里是正方形
    ax.scatter(xCord1, yCord1, s=30, c='red', marker='s')
    # 同上说明
    ax.scatter(xCord2, yCord2, s=30, c='green')
    # 生成一个[-3.0, 3.0]范围中间隔每0.1取一个值
    x = arange(-3.0, 3.0, 0.1)
    # y相对于x的函数
    """
    这里的y是怎么得到的呢？
        从dataMat.append([1.0, float(lineArr[0]), float(lineArr[1])])
        可得：w0*x0+w1*x1+w2*x2 = z
        x0最开始就设置为1，x2就是我们画图的y值，x1就是我们画图的x值。
        所以：w0 + w1*x + w2*y = 0
        →   y = (-w0 - w1 * x) / w2
    """
    y = (-weights[0] - weights[1] * x) / weights[2]
    # 画线
    ax.plot(x, y)
    # 设置x轴和y轴的名称
    plt.xlabel('x1')
    plt.ylabel('x2')
    # 展示图像
    plt.show()


def classifyVector(inX, weights):
    """
    使用梯度上升算法获取到的最优系数来计算测试样本中对应的Sigmoid值。
    其中Sigmoid值大于0.5返回1，小于0.5返回0.
    :param inX: 特征数组
    :param weights: 最优系数
    :return: 返回分类结果，即1或0
    """

    prob = sigmoid(sum(inX * weights))
    if prob > 0.5:
        return 1.0
    else:
        return 0.0


def colicTest():
    """
    测试回归系数算法的用于计算疝气病症预测病马的死亡率的错误率。
    这里运用的随机梯度算法来获取最佳系数w1，w2,...,wn
    :return: 返回此次测试的错误率
    """

    # 以默认只读方式打开训练数据样本和测试数据样本
    frTrain = open('data/horseColicTraining.txt')
    frTest = open('data/horseColicTest.txt')
    trainingSet = []
    trainingLabels = []
    # 读取训练数据样本的每一行
    for line in frTrain.readlines():
        # 去掉首尾空格，并按tab空格数来切割字符串，并将切割后的值存入列表
        currLine = line.strip().split('\t')
        lineArr = []
        # 将21个特征值依次加入到lineArr列表汇总
        for i in range(21):
            lineArr.append(float(currLine[i]))
        # 再将lineArr列表加入到二维列表trainingSet列表中
        trainingSet.append(lineArr)
        # 将类型值依次接到trainingLabels这个列表的末尾行
        trainingLabels.append(float(currLine[21]))
    # 使用上面写的改进的随机梯度算法求得最佳系数，用于下面分类器使用区分类型
    trainWeights = stocGradAscent1(array(trainingSet), trainingLabels, 300)
    errorCount = 0
    numTestVec = 0.0
    # 读取测试数据的每一行
    for line in frTest.readlines():
        # 测试数据数加1
        numTestVec += 1.0
        # 去掉首尾空格，并以tab空格数切割字符串，并将切割后的值存入列表
        currLine = line.strip().split('\t')
        lineArr = []
        # 将21个特征值依次加入到特征列表lineArr中
        for i in range(21):
            lineArr.append(float(currLine[i]))
        # 通过上面计算得到的最佳系数，使用分类器计算lineArr这些特征下的所属的类型
        if int(classifyVector(array(lineArr), trainWeights)) != int(currLine[21]):
            # 如果分类器得到结果和真实结果不符，则错误次数加1
            errorCount += 1
    # 通过遍历获得的所有测试数据量和错误次数求得最终的错误率
    errorRate = float(errorCount) / numTestVec
    # 输出错误率
    print("测试结果的错误率为：{:.2%}".format(errorRate))
    # 返回错误率，用于计算n次错误率的平均值
    return errorRate


def multiTest():
    """
    多次测试算法的错误率取平均值，以得到一个比较有说服力的结果。
    :return:
    """

    numTests = 10
    errorSum = 0.0
    # 通过10次的算法测试，并获得10次错误率的总和
    for k in range(numTests):
        errorSum += heartTest()
    # 通过错误率总和/10可以求得10次平均错误率并输出
    print("10次算法测试后平均错误率为：{:.2%}".format(errorSum/float(numTests)))


def movieTest():
    """
    使用Logistic回归算法测试判断电影类别的错误率。
    :return: 错误率
    """

    trainFile = open("data/movieTraining.txt")
    testFile = open("data/movieTest.txt")
    trainSet = []
    trainLabels = []
    for line in trainFile.readlines():
        lineArr = line.strip().split('\t')
        trainSet.append([float(lineArr[0]), float(lineArr[1])])
        trainLabels.append(float(lineArr[2]))
    trainWeights = stocGradAscent1(array(trainSet), trainLabels, 500)
    errorCount = 0
    allTestCount = 0
    for line in testFile.readlines():
        allTestCount += 1
        lineArr = line.strip().split('\t')
        eigenvalue = [float(lineArr[0]), float(lineArr[1])]
        if classifyVector(eigenvalue, trainWeights) != float(lineArr[2]):
            errorCount += 1
    errorRate = float(errorCount)/float(allTestCount)
    print("错误率为：{:.2%}".format(errorRate))
    return errorRate


def heartTest():
    """
    使用Logistic回归算法诊断心脏病的错误率。
    :return:错误率
    """

    trainFile = open("data/heartTraining.txt")
    testFile = open("data/heartTest.txt")
    trainSet = []
    trainLabels = []
    for line in trainFile.readlines():
        lineArr = line.strip().split('\t')
        temArr = []
        for i in range(13):
            temArr.append(float(lineArr[i]))
        trainSet.append(temArr)
        trainLabels.append(float(lineArr[13]))
    trainWeights = stocGradAscent1(array(trainSet), trainLabels, 500)
    errorCount = 0
    allTestCount = 0
    for line in testFile.readlines():
        allTestCount += 1
        lineArr = line.strip().split('\t')
        eigenvalue = []
        for i in range(13):
            eigenvalue.append(float(lineArr[i]))
        if classifyVector(eigenvalue, trainWeights) != float(lineArr[13]):
            errorCount += 1
    errorRate = float(errorCount) / float(allTestCount)
    print("错误率为：{:.2%}".format(errorRate))
    return errorRate


def dataTest(trainFileName, testFileName, numOfFeatures):
    """
    函数功能：测试回归算法预测数据样本的错误率。
    函数伪代码：
        1. 读取训练样本中的数据，进行格式化处理；
        2. 将格式化处理后的数据传入随机梯度上升算法函数中，获取到最佳参数。
        3. 再读取测试样本中的数据，进行格式化处理后，调用分类器函数（传入样本特征和最佳参数），可以预测出最终特征。与测试数据中的实际特征进行比较，计算出错误次数。
        4. 最终通过错误次数/测试样本总数**求出错误率。
    :param trainFileName: 训练样本的文件路径/文件名
    :param testFileName: 测试样本的文件路径/文件名
    :param numOfFeatures: 样本所包含的特征数量
    :return:
    """

    trainFile = open(trainFileName)
    testFile = open(testFileName)
    trainSet = []
    trainLabels = []
    for line in trainFile.readlines():
        lineArr = line.strip().split('\t')
        temArr = []
        for i in range(numOfFeatures):
            temArr.append(float(lineArr[i]))
        trainSet.append(temArr)
        trainLabels.append(float(lineArr[numOfFeatures]))
    trainWeights = stocGradAscent1(array(trainSet), trainLabels, 200)
    errorCount = 0
    allTestCount = 0
    for line in testFile.readlines():
        allTestCount += 1
        lineArr = line.strip().split('\t')
        eigenvalue = []
        for i in range(numOfFeatures):
            eigenvalue.append(float(lineArr[i]))
        if classifyVector(eigenvalue, trainWeights) != float(lineArr[numOfFeatures]):
            errorCount += 1
    errorRate = float(errorCount) / float(allTestCount)
    print("错误率为：{:.2%}".format(errorRate))
    return errorRate


def multiTest1(trainFileName, testFileName, numOfFeatures):
    """
    多次测试算法的错误率取平均值，以得到一个比较有说服力的结果。
    :return:
    """

    numTests = 10
    errorSum = 0.0
    # 通过10次的算法测试，并获得10次错误率的总和
    for k in range(numTests):
        errorSum += dataTest(trainFileName, testFileName, numOfFeatures)
    # 通过错误率总和/10可以求得10次平均错误率并输出
    print("10次算法测试后平均错误率为：{:.2%}".format(errorSum/float(numTests)))


if __name__ == '__main__':
    # dataMats, classMats = loadDataSet()
    # dataArr = array(dataMats)
    # weights = array(gradAscent(dataMats, classMats))
    # print(weights)
    # plotBestFit(dataArr, classMats, weights)
    # weights = array(stocGradAscent0(dataArr, classMats))
    # plotBestFit(dataArr, classMats, weights)
    # weights = array(stocGradAscent1(dataArr, classMats))
    # plotBestFit(dataArr, classMats, weights)
    # multiTest()
    print("示例1：示例1：从疝气病症预测病马的死亡率")
    multiTest1('data/horseColicTraining.txt', 'data/horseColicTest.txt', 21)
    print("\n示例2：从打斗数和接吻数预测电影类型")
    multiTest1('data/movieTraining.txt', 'data/movieTest.txt', 2)
    print("\n示例3：从心脏检查样本帮助诊断心脏病")
    multiTest1('data/heartTraining.txt', 'data/heartTest.txt', 13)
